/* 
 * This file is part of Last.fm Now Playing Plugi-in for Adium
 * Copyright (c) 2011 Anton Bronnikov <anton.bronnikov@gmail.com>.
 * 
 * Last.fm Now Playing Plugi-in for Adium is free software: you can redistribute
 * it and/or modify it under the terms of the GNU General Public License as 
 * published by the Free Software Foundation, either version 3 of the License, 
 * or (at * your option) any later version.
 * 
 * Last.fm Now Playing Plugi-in for Adium is distributed in the hope that it 
 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General 
 * Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with 
 * Last.fm Now Playing Plugi-in for Adium. If not, see 
 * <http://www.gnu.org/licenses/>.
 *
 */

#import "LastFmNowPlayingObserver.h"
#import "LastFmApiKey.h"
#import <Adium/ESDebugAILog.h>

NSInteger      const LASTFM_POLLING_INTERVAL = 30;
NSTimeInterval const LASTFM_TIMEOUT_INTERVAL = 15;

NSString * const LASTFM_GETUSERINFO    = @"http://ws.audioscrobbler.com/2.0/?method=user.getinfo&user=%@&api_key=%@";
NSString * const LASTFM_GETRECENTTRACK = @"http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=%@&limit=1&api_key=%@";

NSString * const LASTFM_XPATH_GETSTATUS    = @"/lfm[1]/@status";
NSString * const LASTFM_XPATH_GETERROR     = @"/lfm[1]/error[1]";
NSString * const LASTFM_XPATH_GETERRORCODE = @"/lfm[1]/error[1]/@code";
NSString * const LASTFM_XPATH_NOWPLAYING   = @"/lfm[1]/recenttracks[1]/track[1]/@nowplaying";
NSString * const LASTFM_XPATH_NAME         = @"/lfm[1]/recenttracks[1]/track[1]/name[1]";
NSString * const LASTFM_XPATH_ARTIST       = @"/lfm[1]/recenttracks[1]/track[1]/artist[1]";
NSString * const LASTFM_XPATH_ALBUM        = @"/lfm[1]/recenttracks[1]/track[1]/album[1]";
NSString * const LASTFM_XPATH_URL          = @"/lfm[1]/recenttracks[1]/track[1]/url[1]";

NSString * const LASTFM_OK   = @"ok";
NSString * const LASTFM_TRUE = @"true";

#pragma mark -
#pragma mark Private interface : LastFmNowPlayingObserver

@interface LastFmNowPlayingObserver(private)

#pragma mark -
#pragma mark Properties

@property (assign, readwrite) BOOL isNowPlaying;
@property (retain, readwrite) LastFmTrack * nowPlaying;

#pragma mark -
#pragma mark Methods

- (NSTimer *) newLastFmPollingTimer;
- (NSXMLDocument *) queryLastFmWith:(NSString *)request error:(NSError **)requestError;
- (NSString *) extractStringByXPath:(NSString *)xPath fromXmlDocument:(NSXMLDocument *)xmlDocument error:(NSError **)xmlError;
- (void) pollLastFm;

@end

#pragma mark -
#pragma mark Implementation : LastFmNowPlayingObserver

@implementation LastFmNowPlayingObserver

#pragma mark -
#pragma mark Synthesized properties

@synthesize delegate     = _delegate;
@synthesize lastFmUserID = _lastFmUserID;
@synthesize silence      = _silence;

#pragma mark -
#pragma mark Property : isEnabled

- (BOOL) isEnabled {
  AILogWithSignature(@"isEnabled == %i", _isEnabled);
  
  return _isEnabled;
}

- (void) setIsEnabled:(BOOL)newIsEnabled {
  // Do nothing if the state does not chage
  if ((newIsEnabled && _isEnabled) || (!newIsEnabled && !_isEnabled)) {
    return;
  }
  
  
  if (!newIsEnabled) {                          // If the new state is DISABLED
    // Kill the timer if it is running
    if (_lastFmPollingTimer) {
      [_lastFmPollingTimer invalidate];
      [_lastFmPollingTimer release];
      _lastFmPollingTimer = nil;
    }
    
    // Set now-playing state to NOT-PLAYING
    self.isNowPlaying = NO;
    self.nowPlaying = self.silence;
  } else {                                      // If the new state is ENABLED]
    // Start up the timer if it is not running already (which normally should not be the case)
    if (!_lastFmPollingTimer || ![_lastFmPollingTimer isValid]) {
      _lastFmPollingTimer = [self newLastFmPollingTimer];
    }
  }
  
  // Finally, do the assignment
  _isEnabled = newIsEnabled;
  
  AILogWithSignature(@"isEnabled = %i", _isEnabled);
}

#pragma mark -
#pragma mark Property : isNowPlaying

- (BOOL) isNowPlaying {
  AILogWithSignature(@"isNowPlaying == %i", _isNowPlaying);
  
  return _isNowPlaying;
}

- (void) setIsNowPlaying:(BOOL)newIsNowPlaying {
  // Do nothing if the state did not change
  if ((newIsNowPlaying && _isNowPlaying) || (!newIsNowPlaying && !_isNowPlaying)) {
    return;
  }
  
  // Do the assignment
  _isNowPlaying = newIsNowPlaying;
  
  // Notify the delegate (if any)
  NSObject <LastFmNowPlayingObserverDelegate> * tempDelegate = self.delegate;
  
  if (tempDelegate) {
    if (_isNowPlaying) {
      if ([tempDelegate respondsToSelector:@selector(playingStarted:)]) {
        [tempDelegate playingStarted:self];
      }
    } else {
      if ([tempDelegate respondsToSelector:@selector(playingStopped:)]) {
        [tempDelegate playingStopped:self];
      }
    }
  }
  
  AILogWithSignature(@"isNowPlaying = %i", _isNowPlaying);
}

#pragma mark -
#pragma mark Property : nowPlaying

- (LastFmTrack *) nowPlaying {
  AILogWithSignature(@"nowPlaying == %@", _nowPlaying);
  
  return [[_nowPlaying retain] autorelease];
}

- (void) setNowPlaying:(LastFmTrack *)newNowPlaying {
  // Do nothing if the pointers are the same
  if (_nowPlaying == newNowPlaying) {
    return;
  }
  
  // Do nothing if the values are the same
  if ([_nowPlaying isEqualToLastFmTrack:newNowPlaying]) {
    return;
  }
  
  // Now that there is a difference, do something
  [_nowPlaying release];
  _nowPlaying = [newNowPlaying retain];
  
  // And notify the delegate (if any)
  NSObject <LastFmNowPlayingObserverDelegate> * tempDelegate = self.delegate;
  
  if (tempDelegate && [tempDelegate respondsToSelector:@selector(playingNewTrack:)]) {
    [tempDelegate playingNewTrack:self];
  }
  
  AILogWithSignature(@"nowPlaying = %@", _nowPlaying);
}

#pragma mark -
#pragma mark Timer management

- (NSTimer *) newLastFmPollingTimer {
  AILogWithSignature(@"Creating a new Last.fm polling timer");
  
  // Create and retain (new-method signature) the timer
  NSTimer * newTimer = [NSTimer scheduledTimerWithTimeInterval:LASTFM_POLLING_INTERVAL 
                                                        target:self 
                                                      selector:@selector(pollLastFm) 
                                                      userInfo:nil 
                                                       repeats:YES];
  [newTimer retain];
  
  // Schedule it to fire so that the state is refreshed immediately
  [newTimer performSelector:@selector(fire) withObject:nil afterDelay:1];
  
  // And finally pass it to the happy parents
  return newTimer;
}

#pragma mark -
#pragma mark Last.fm communication

- (NSXMLDocument *) queryLastFmWith:(NSString *)request error:(NSError **)requestError {
  static BOOL enteringIsClean = YES;
  
  if (enteringIsClean) {
    enteringIsClean = NO;
    
    AILogWithSignature(@"Querying Last.fm with \'%@\'", request);
    
    // Create URL for polling Last.fm
    NSURL * requestURL = [NSURL URLWithString:request];
    
    if (!requestURL) {
      AILogWithSignature(@"Could not create NSURL object for path\'%@\'", request);
      enteringIsClean = YES;
      return nil;
    }
    
    // Get the XML data from Last.fm
    NSXMLDocument * response = nil;
    
    response = [[NSXMLDocument alloc] initWithContentsOfURL:requestURL
                                                    options:(NSXMLNodePreserveWhitespace|NSXMLNodePreserveCDATA)
                                                      error:requestError];
    if (!response) {
      response = [[NSXMLDocument alloc] initWithContentsOfURL:requestURL
                                                      options:NSXMLDocumentTidyXML
                                                        error:requestError];
    }
    
    if (!response) {
      AILogWithSignature(@"Request to Last.fm failed with error %@", (* requestError));
    }
    
    enteringIsClean = YES;
    return [response autorelease];
  } else {
    return nil;
  }
}

- (NSString *) extractStringByXPath:(NSString *)xPath fromXmlDocument:(NSXMLDocument *)xmlDocument error:(NSError **)xmlError {
  AILogWithSignature(@"Extracting XPath \'%@\' from \'%@\'", xPath, xmlDocument);
  
  NSString * extractedString = nil;
  
  NSArray * xmlNodes = [xmlDocument nodesForXPath:xPath error:xmlError];
  
  if (xmlNodes && [xmlNodes count] > 0) {
    NSXMLNode * firstXmlNode = [xmlNodes objectAtIndex:0];
    
    if ([firstXmlNode isKindOfClass:[NSXMLNode class]]) {
      extractedString = [firstXmlNode stringValue];
    } else {
      AILogWithSignature(@"XPath \'%@\' returned wrong class \'%@\'", xPath, [firstXmlNode class]);
    }
  } else {
    AILogWithSignature(@"XPath \'%@\' returned no data with error \'%@\'", xPath, *xmlError);    
  }
  
  return extractedString;
}

- (BOOL) validateLastFmUserID:(NSString *)someLastFmUserID {
  AILogWithSignature(@"Validating Last.fm user ID \'%@\'", someLastFmUserID);
  
  // Get the XML data from Last.fm
  NSError * error = nil;
  NSXMLDocument * xmlResponse = nil;
  
  xmlResponse = [self queryLastFmWith:[NSString stringWithFormat:LASTFM_GETUSERINFO, self.lastFmUserID, LASTFM_APIKEY] error:&error];
  
  // If no response, say NO
  if (!xmlResponse) {
    return NO;
  }
  
  // Check the status of the request
  NSString * queryStatus = [self extractStringByXPath:LASTFM_XPATH_GETSTATUS fromXmlDocument:xmlResponse error:&error];
  
  if (!queryStatus || ![queryStatus isEqualToString:LASTFM_OK]) {
    AILogWithSignature(@"Last response status was \'%@\'", queryStatus);
    
    NSString * lastFmErrorDescription = [self extractStringByXPath:LASTFM_XPATH_GETERROR fromXmlDocument:xmlResponse error:nil];
    NSString * lastFmErrorCode = [self extractStringByXPath:LASTFM_XPATH_GETERRORCODE fromXmlDocument:xmlResponse error:nil];
    
    AILogWithSignature(@"Last.fm error %@ (%@)", lastFmErrorDescription, lastFmErrorCode);
    
    return NO;
  }
  
  // And now we really can say YES  
  return YES;
}

- (void) pollLastFm {
  AILogWithSignature(@"Polling Last.fm");
  
  // Get the XML data from Last.fm
  NSError * error = nil;
  NSXMLDocument * xmlResponse = nil;
  
  xmlResponse = [self queryLastFmWith:[NSString stringWithFormat:LASTFM_GETRECENTTRACK, self.lastFmUserID, LASTFM_APIKEY] error:&error];
  
  // If no response log the error and mute
  if (!xmlResponse) {
    self.isNowPlaying = NO;
    self.nowPlaying = self.silence;
    return;
  }
  
  // Check the status of the request
  NSString * queryStatus  = [self extractStringByXPath:LASTFM_XPATH_GETSTATUS 
                                       fromXmlDocument:xmlResponse 
                                                 error:&error];
  
  if (!queryStatus || ![queryStatus isEqualToString:LASTFM_OK]) {
    AILogWithSignature(@"Last response status was \'%@\'", queryStatus);

    NSString * lastFmErrorDescription = [self extractStringByXPath:LASTFM_XPATH_GETERROR fromXmlDocument:xmlResponse error:nil];
    NSString * lastFmErrorCode = [self extractStringByXPath:LASTFM_XPATH_GETERRORCODE fromXmlDocument:xmlResponse error:nil];
    
    AILogWithSignature(@"Last.fm error %@ (%@)", lastFmErrorDescription, lastFmErrorCode);
    
    self.isNowPlaying = NO;
    self.nowPlaying = self.silence;
    return;
  }
  
  // Is there anything playing now  
  NSString * lastFmIsNowPlaying = [self extractStringByXPath:LASTFM_XPATH_NOWPLAYING 
                                             fromXmlDocument:xmlResponse 
                                                       error:&error];
  
  if (!lastFmIsNowPlaying || ![lastFmIsNowPlaying isEqualToString:LASTFM_TRUE]) {
    AILogWithSignature(@"There is nothing playing right now (\'%@\')", lastFmIsNowPlaying);
    
    self.isNowPlaying = NO;
    self.nowPlaying = self.silence;
    return;
  }
  
  // Get the track title & artist
  NSString * trackName   = [self extractStringByXPath:LASTFM_XPATH_NAME fromXmlDocument:xmlResponse error:&error];
  NSString * trackArtist = [self extractStringByXPath:LASTFM_XPATH_ARTIST fromXmlDocument:xmlResponse error:&error];
  NSString * trackAlbum  = [self extractStringByXPath:LASTFM_XPATH_ALBUM fromXmlDocument:xmlResponse error:&error];
  NSString * trackURL    = [self extractStringByXPath:LASTFM_XPATH_URL fromXmlDocument:xmlResponse error:&error];
  
  // Check if the track has changed
  if ([self.nowPlaying.title isEqualToString:trackName] && [self.nowPlaying.artist isEqualToString:trackArtist]) {
    AILogWithSignature(@"Still playing the same track");
    return;
  }
  
  // Update now-playing info
  self.isNowPlaying = YES;
  self.nowPlaying = [[[LastFmTrack alloc] initWithName:trackName artist:trackArtist album:trackAlbum url:trackURL] autorelease];
}

#pragma mark -
#pragma mark Object life-cycle

- (id) initWithLastFmUserID:(NSString *)initLastFmUserID state:(BOOL)initIsEnabled {
  self = [super init];
  
  if (self) {
    _lastFmUserID = [initLastFmUserID copy];
    _isEnabled = initIsEnabled;
    _isNowPlaying = NO;
    _silence = [[LastFmTrack alloc] initWithName:nil artist:nil album:nil url:nil];
    _nowPlaying = [_silence retain];
    
    if (_isEnabled) {
      _lastFmPollingTimer = [self newLastFmPollingTimer];
    } else {
      _lastFmPollingTimer = nil;
    }
  }
  
  return self;
}

- (void) dealloc {
  if (_delegate) {
    _delegate = nil;
  }
  
  if (_silence) {
    [_silence release];
    _silence = nil;
  }
  
  if (_nowPlaying) {
    [_nowPlaying release];
    _nowPlaying = nil;
  }
  
  if (_lastFmPollingTimer) {
    [_lastFmPollingTimer invalidate];
    [_lastFmPollingTimer release];
    _lastFmPollingTimer = nil;
  }
  
  [super dealloc];
}

@end
